﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections;
using System.Collections.Generic;
using System.CommandLine.Completions;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace System.CommandLine
{
    /// <summary>
    /// Represents a specific action that the application performs.
    /// </summary>
    /// <remarks>
    /// Use the Command object for actions that correspond to a specific string (the command name).
    /// For simple applications that only have one action, see <see cref="RootCommand"/>.
    /// For example, <c>dotnet run</c> uses <c>run</c> as the command.
    /// </remarks>
    public class Command : Symbol, IEnumerable
    {
        internal AliasSet? _aliases;
        private ChildSymbolList<Argument>? _arguments;
        private ChildSymbolList<Option>? _options;
        private ChildSymbolList<Command>? _subcommands;
        private List<Action<CommandResult>>? _validators;

        /// <summary>
        /// Initializes a new instance of the Command class.
        /// </summary>
        /// <param name="name">The name of the command.</param>
        /// <param name="description">The description of the command, shown in help.</param>
        public Command(string name, string? description = null) : base(name)
            => Description = description;

        /// <summary>
        /// Gets the child symbols.
        /// </summary>
        public IEnumerable<Symbol> Children
        {
            get
            {
                foreach (var command in Subcommands)
                    yield return command;

                foreach (var option in Options)
                    yield return option;

                foreach (var argument in Arguments)
                    yield return argument;
            }
        }

        /// <summary>
        /// Gets all of the arguments for the command.
        /// </summary>
        public IList<Argument> Arguments => _arguments ??= new(this);

        internal bool HasArguments => _arguments?.Count > 0 ;

        /// <summary>
        /// Gets all of the options for the command.
        /// </summary>
        /// <remarks>
        /// This collection doesn't include options on parent commands where <see cref="Option.Recursive">Option.Recursive</see> is <see langword="true" />. Those options are valid under the current command but don't appear in this collection.
        /// </remarks>
        public IList<Option> Options => _options ??= new (this);

        internal bool HasOptions => _options?.Count > 0;

        /// <summary>
        /// Gets all of the subcommands for the command.
        /// </summary>
        public IList<Command> Subcommands => _subcommands ??= new(this);

        internal bool HasSubcommands => _subcommands is not null && _subcommands.Count > 0;

        /// <summary>
        /// Gets the validators to the command. Validators can be used
        /// to create custom validation logic.
        /// </summary>
        public List<Action<CommandResult>> Validators => _validators ??= new ();

        internal bool HasValidators => _validators is not null && _validators.Count > 0;

        /// <summary>
        /// Gets the unique set of strings that can be used on the command line to specify the command.
        /// </summary>
        /// <remarks>The collection does not contain the <see cref="Symbol.Name"/> of the Command.</remarks>
        public ICollection<string> Aliases => _aliases ??= new();

        /// <summary>
        /// Gets or sets the <see cref="CommandLineAction"/> for the Command. The handler represents the action
        /// that will be performed when the Command is invoked.
        /// </summary>
        /// <remarks>
        /// <para>Use one of the <see cref="SetAction(Action{ParseResult})" /> overloads to construct a handler.</para>
        /// <para>If the handler is not specified, parser errors will be generated for command line input that
        /// invokes this Command.</para></remarks>
        public CommandLineAction? Action { get; set; }

        /// <summary>
        /// Sets a synchronous action to be run when the command is invoked.
        /// </summary>
        public void SetAction(Action<ParseResult> action)
        {
            if (action is null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            Action = new AnonymousSynchronousCommandLineAction(context =>
            {
                action(context);
                return 0;
            });
        }

        /// <summary>
        /// Sets a synchronous action to be run when the command is invoked.
        /// </summary>
        /// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
        public void SetAction(Func<ParseResult, int> action)
        {
            if (action is null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            Action = new AnonymousSynchronousCommandLineAction(action);
        }

        /// <summary>
        /// Sets an asynchronous action to be run when the command is invoked.
        /// </summary>
        public void SetAction(Func<ParseResult, CancellationToken, Task> action)
        {
            if (action is null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            Action = new AnonymousAsynchronousCommandLineAction(async (context, cancellationToken) =>
            {
                await action(context, cancellationToken);
                return 0;
            });
        }

        /// <summary>
        /// Sets an asynchronous action to be run when the command is invoked.
        /// </summary>
        /// <remarks>
        /// When possible, prefer using the <see cref="SetAction(Func{ParseResult, CancellationToken, Task})"/> overload
        /// and passing the <see cref="CancellationToken"/> parameter to the async method(s) called by the action.
        /// </remarks>
        // Hide from intellisense, it's public to avoid the compiler choosing a sync overload
        // for an async action (and fire and forget issue described in https://github.com/dotnet/command-line-api/issues/2562).
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void SetAction(Func<ParseResult, Task> action)
        {
            if (action is null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            Action = new AnonymousAsynchronousCommandLineAction(async (context, _) =>
            {
                await action(context);
                return 0;
            });
        }

        /// <summary>
        /// Sets an asynchronous action to be run when the command is invoked.
        /// </summary>
        /// <remarks>
        /// When possible, prefer using the <see cref="SetAction(Func{ParseResult, CancellationToken, Task{int}})"/> overload
        /// and passing the <see cref="CancellationToken"/> parameter to the async method(s) called by the action.
        /// </remarks>
        // Hide from intellisense, it's public to avoid the compiler choosing a sync overload
        // for an async action (and fire and forget issue described in https://github.com/dotnet/command-line-api/issues/2562).
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void SetAction(Func<ParseResult, Task<int>> action)
        {
            if (action is null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            Action = new AnonymousAsynchronousCommandLineAction(async (context, _) =>
            {
                return await action(context);
            });
        }

        /// <summary>
        /// Sets an asynchronous action when the command is invoked.
        /// </summary>
        /// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
        public void SetAction(Func<ParseResult, CancellationToken, Task<int>> action)
        {
            if (action is null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            Action = new AnonymousAsynchronousCommandLineAction(action);
        }

        /// <summary>
        /// Adds a <see cref="Argument"/> to the command.
        /// </summary>
        /// <param name="argument">The option to add to the command.</param>
        public void Add(Argument argument) =>  Arguments.Add(argument);

        /// <summary>
        /// Adds an <see cref="Option"/> to the command.
        /// </summary>
        /// <param name="option">The option to add to the command.</param>
        public void Add(Option option) =>  Options.Add(option);

        /// <summary>
        /// Adds a <see cref="Command"/> to the command.
        /// </summary>
        /// <param name="command">The Command to add to the command.</param>
        public void Add(Command command) => Subcommands.Add(command);

        /// <summary>
        /// Gets or sets a value that indicates whether unmatched tokens should be treated as errors.
        /// </summary>
        /// <value><see langword="true"/> to fail validation when an extra command or argument is provided; otherwise, <see langword="false"/>.</value>
        public bool TreatUnmatchedTokensAsErrors { get; set; } = true;

        /// <inheritdoc />
        [DebuggerStepThrough]
        [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public for C# collection initializer
        IEnumerator IEnumerable.GetEnumerator() => Children.GetEnumerator();

        /// <summary>
        /// Parses an array strings using the command.
        /// </summary>
        /// <param name="args">The string arguments to parse.</param>
        /// <param name="configuration">The configuration on which the parser's grammar and behaviors are based.</param>
        /// <returns>A parse result describing the outcome of the parse operation.</returns>
        public ParseResult Parse(IReadOnlyList<string> args, ParserConfiguration? configuration = null)
            => CommandLineParser.Parse(this, args, configuration);

        /// <summary>
        /// Parses a command line string value using the command.
        /// </summary>
        /// <remarks>The command line string input will be split into tokens as if it had been passed on the command line.</remarks>
        /// <param name="commandLine">A command line string to parse, which can include spaces and quotes equivalent to what can be entered into a terminal.</param>
        /// <param name="configuration">The configuration on which the parser's grammar and behaviors are based.</param>
        /// <returns>A parse result describing the outcome of the parse operation.</returns>
        public ParseResult Parse(string commandLine, ParserConfiguration? configuration = null)
            => CommandLineParser.Parse(this, commandLine, configuration);

        /// <inheritdoc />
        public override IEnumerable<CompletionItem> GetCompletions(CompletionContext context)
        {
            var completions = new List<CompletionItem>();

            if (context.WordToComplete is { } textToMatch)
            {
                if (HasSubcommands)
                {
                    var commands = Subcommands;
                    for (int i = 0; i < commands.Count; i++)
                    {
                        AddCompletionsFor(commands[i], commands[i]._aliases);
                    }
                }

                if (HasOptions)
                {
                    var options = Options;
                    for (int i = 0; i < options.Count; i++)
                    {
                        AddCompletionsFor(options[i], options[i]._aliases);
                    }
                }

                if (HasArguments)
                {
                    var arguments = Arguments;
                    for (int i = 0; i < arguments.Count; i++)
                    {
                        var argument = arguments[i];
                        foreach (var completion in argument.GetCompletions(context))
                        {
                            if (completion.Label.ContainsCaseInsensitive(textToMatch))
                            {
                                completions.Add(completion);
                            }
                        }
                    }
                }

                SymbolNode? parent = FirstParent;
                while (parent is not null)
                {
                    Command parentCommand = (Command)parent.Symbol;

                    if (context.IsEmpty || context.ParseResult.GetResult(parentCommand) is not null)
                    {
                        if (parentCommand.HasOptions)
                        {
                            for (var i = 0; i < parentCommand.Options.Count; i++)
                            {
                                var option = parentCommand.Options[i];

                                if (option.Recursive)
                                {
                                    AddCompletionsFor(option, option._aliases);
                                }
                            }
                        }
                        parent = parent.Symbol.FirstParent;
                    }
                    else
                    {
                        parent = parent.Next;
                    }
                }
            }

            return completions
                   .OrderBy(item => item.SortText.IndexOfCaseInsensitive(context.WordToComplete))
                   .ThenBy(symbol => symbol.Label, StringComparer.OrdinalIgnoreCase);

            void AddCompletionsFor(Symbol identifier, AliasSet? aliases)
            {
                if (!identifier.Hidden)
                {
                    if (identifier.Name.ContainsCaseInsensitive(textToMatch))
                    {
                        completions.Add(new CompletionItem(identifier.Name, CompletionItem.KindKeyword, detail: identifier.Description));
                    }

                    if (aliases is not null)
                    {
                        foreach (string alias in aliases)
                        {
                            if (alias.ContainsCaseInsensitive(textToMatch))
                            {
                                completions.Add(new CompletionItem(alias, CompletionItem.KindKeyword, detail: identifier.Description));
                            }
                        }
                    }
                }
            }
        }

        internal bool EqualsNameOrAlias(string name)
            => Name.Equals(name, StringComparison.Ordinal) || (_aliases is not null && _aliases.Contains(name));
    }
}
